OLEAUTO.TXT Supplementary documentation for TOLEAutoClient component ************************************************************ For support, send E-mail to: sraike@iconz.co.nz or CompuServe: 100236,1656 or send FAX to: 64-9-832-0088 ************************************************************ This file describes how to write a "mirror" class for Delphi that lets your application access properties and methods of an OLE Automation server just as if they were properties and methods of one of your own objects. Since TOleAutoClient was first released in April 1995, filling the gap left by Delphi's lack of OLE Automation support, customers have used it to control a wide variety of OLE Automation servers including Microsoft Office products, ABC Flowcharter, Visio, Acrobat, Watermark and a number of others. (All these names are the property of their respective trademark owners.) One description here uses Microsoft Word for Windows 6.0 as a typical OLE Automation server. "Word", "Excel", "Word for Windows", "Windows" and "Visual Basic" are registered trademarks of Microsoft Corporation. For more information, refer to the README.1ST and OLEAUTO.INT files and to the accompanying compressed example files like WORDDRIV.ZIP, NUMSVR.ZIP and VISIOOLE.ZIP that contain working sample code. Many servers "expose" a hierarchy of nested OLE objects and classes. After reading the introductory material in this file, you should refer to the file NESTOBJ.TXT for a discussion of how to write mirror classes that describe such nested objects. You should also refer to the WHATSNEW.TXT file for a description of the features incorporated in the latest release of TOLEAutoClient. ****************************************** OVERVIEW ****************************************** Some Windows development tools (Microsoft Visual Basic, for one) let you control OLE Automation servers (like Microsoft Word 6.0, for example). An OLE Automation server "exposes" certain properties and methods for access by other programs. Microsoft Word 6.0 exposes most of its WordBasic macro language; other servers expose properties, objects and methods as detailed in their documentation. For Word 6.0, you should refer to Microsoft documentation such as its online Help and the Microsoft Office Developers' Kit (ODK). For example, among the WordBasic methods Word exposes are: FileNew - tells Word to open a new file ToolsOptionsEdit - sets Tools|Edit|Options dialog items ToolsMacro - tells Word to run a template macro FileClose - tells Word to close a current file ****************************************** EXAMPLE 1 - (WORDDRIV.ZIP) ****************************************** Suppose you want your program to tell Word to open a new file based on the NORMAL.DOT template, set and clear some check boxes on the Edit tab of the Tools|Options dialog, run a WordBasic macro called DummyMsg that is stored in the NORMAL.DOT template, and then close the file without saving it. Referring to the Word documentation, you discover that the WordBasic methods you need to call, with their parameters, are: FileNew "NORMAL.DOT" ToolsOptionsEdit 1,1,0,0,0,0,0,0,0 ToolsMacro "DummyMsg", 1 FileClose 2 (WordBasic actually allows you to use named arguments like .Run = 1 instead of "positional" arguments; neither TOLEAutoClient nor Visual Basic yet supports named arguments. Also, be aware that WordBasic online Help sometimes fails to list the named arguments in their correct positional order; this information is given in the file POSITION.TXT in the Office Developers' Kit. Alternatively, you can record a WordBasic macro and then look at the macro code it generates for the correct parameter order.) If you were writing Visual Basic code to do this, it would look like: Dim ob As Object Set ob = CreateObject("word.basic") ob.FileNew "NORMAL.DOT" ob.ToolsOptionsEdit 1,1,0,0,0,0,0,"Microsoft Word" ob.ToolsMacro "DummyMsg", 1 ob.FileClose 2 Set ob = Nothing ... Using the TOleAutomationClient component, you'll write the following code in Delphi to accomplish the same thing: var WordOb : TWordObj; begin WordOb := TWordObj.CreateObject('word.basic'); WordOb.FileNew('NORMAL.DOT'); WordOb.ToolsOptionsEdit; { The example has hard-coded options...} WordOb.ToolsMacro('DummyMsg'); { Ditto for the second argument...} WordOb.FileClose(2); WordOb.Release; ... But there's a catch! (Read on -- it has to do with the TWordObj class.) Since Delphi is a compiled language, before you can call a method or refer to a property for an object you need to let Delphi know that the object belongs to a class that contains that method or property. The way to do that in Delphi is to create a new unit and in it to declare a new class with the needed methods and properties. Fortunately, the TOleAutoClient component simplifies the process by letting you derive the class from a parent class it supplies: TOleObject. In the case of the example given above, the interface section of the unit looks like: unit WordCls; interface uses SysUtils, OleAuto; { OLEAUTO.DCU must be in \DELPHI\LIB } type TWordObj = class(TOleObject) {TOleObject is defined in OLEAUTO.DCU} public procedure FileNew(TemplateName : String); procedure ToolsMacro(MacroName : String); procedure FileClose(CloseMode : Integer); procedure ToolsOptionsEdit; {arguments omitted for simplicity} end; So that these methods can communicate with Word through OLE Automation, in the implementation section of the WordCls unit you define the class methods you just declared, inserting calls to methods in the parent TOleObject class that pass each of the method arguments and define their types, and that perform the actual OLE method calls. Refer to the file OLEAUTO.INT for a description of the allowable types. In the implementation of a method call, you need to do two things: 1. Pass each argument and define its type. To do this, you call SetOleMethodArg (a member of the parent TOLEObject class) once for each argument to be passed. The order in which the arguments are passed is FROM RIGHT TO LEFT! 2. Have OLE call the method in the server application. To do this, you call either CallOleFunction or CallOleProc, according to whether or not the server method returns a value. In this example, none of the methods returns a value; in the next ones you'll see how to call a method that does return a value, and how to access properties (in addition to methods) exposed by a server. The implementation section looks like this: implementation procedure TWordObj.ToolsOptionsEdit; const pszMSW : PChar = 'Microsoft Word'; var x : Integer; begin { It's necessary to supply ALL the arguments to ToolsOptionsEdit. } { We always set the arguments in reverse order: i.e., right to left. } { For this WordBasic command, all the options indicate check-box status values (0 = unchecked) in the Word 6 Tools|Options\Edit dialog. } SetOleMethodArg('PChar', pszMSW); { .PictureEditor } { The following are just some sample option settings. } x := 0; { Unchecked. } SetOleMethodArg('Integer', x); { .AllowAccentedUppercase } SetOleMethodArg('Integer', x); { .SmartCutPaste } SetOleMethodArg('Integer', x); { .Overtype } SetOleMethodArg('Integer', x); { .InsForPaste } SetOleMethodArg('Integer', x); { .AutoWordSelection } x := 1; { Checked. } SetOleMethodArg('Integer', x); { .DragAndDrop } SetOleMethodArg('Integer', x); { .ReplaceSelection } {After setting the arguments, call the method. This method doesn't return a value, so we use CallOleProc; if it did return a value, we'd use CallOleFunction.} CallOleProc('ToolsOptionsEdit'); end; procedure TWordObj.FileNew(TemplateName : String); var pszName : PChar; begin pszName := StrAlloc(256); { Get some string space. } StrPLCopy(pszName, TemplateName, 40); { Tell Word the name of the template. } SetOleMethodArg('PChar', pszName); CallOleProc('FileNew'); StrDispose(pszName); end; procedure TWordObj.ToolsMacro(MacroName : String); var mode : Integer; pszName : PChar; begin pszName := StrAlloc(256); { Get some string space. } mode := 1; { Tell Word to .Run the macro. } { You might want to write a sample Word macro and save it in the NORMAL.DOT template, perhaps something like: Sub Main MsgBox "Clicked me!" End Sub } { Set the arguments from right to left.} SetOleMethodArg('Integer', mode); { This arg *MUST* be present, even if its type is '' } StrPLCopy(pszName, MacroName, 40); SetOleMethodArg('PChar', pszName); { WordBasic macro name } { Execute it. } CallOleProc('ToolsMacro'); StrDispose(pszName); end; procedure TWordObj.FileClose(CloseMode : Integer); begin SetOleMethodArg('Integer', CloseMode); CallOleProc('FileClose'); end; end. ****************************************** IN-PROCESS SERVERS AND LOCAL SERVERS ****************************************** See the file WHATSNEW.TXT for a discussion of the support provided for controlling in-process OLE Automation servers. ****************************************** CONTROLLING "EMBEDDED" OLE OBJECTS AND ALREADY-RUNNING INSTANCES ****************************************** See the file WHATSNEW.TXT for a discussion of the support provided for this topic. ****************************************** MORE INFORMATION ON ARGUMENT TYPES ****************************************** Internally, all parameters to OLE methods are actually passed as VARIANT types, directly analogous to the Variant type in Microsoft Visual Basic. This type is declared within the OLEAUTO.DCU unit, along with a number of OLE DLL routines for manipulating VARIANT quantities. However, when you pass arguments using SetOleMethodArg, and when you call OLE server methods that return a value, or when you get and set properties of OLE objects, you need to pass a string denoting the type of the argument or return value. See the file OLEAUTO.INT for a list of the allowable Delphi types. You can pass arguments to methods both by value and by reference; a reference argument is equivalent to one declared as var in Pascal. In order to pass an argument by reference, you prefix the name of its type (the string passed as the first parameter to SetOleMethodArg) with the symbol "&". Most servers will expect their arguments to be passed by value, but there are cases when they will accept arguments passed by reference. For example, an OLE (server) method may be designed to modify the values of one or more of its arguments. In order to pass an argument defined as a user-defined type, OLE requires you to pass a pointer to the argument instead. In this case, you should cast the pointer to a generic pointer (i.e., Pointer(myptr)) before passing it with SetOleMethodArg as an argument of type 'Pointer'. The _only_ type names allowed for the first parameter to the SetOleMethodArg method are those listed in OLEAUTO.INT. The same is true for the allowable return types of methods returning values (you specify the return type in the second parameter to CallOleFunction) and for retrieving and setting properties with the GetOleProperty and SetOleProperty methods. Many servers define and "expose" a hierarchy of "nested" objects, collections, etc. Examples are Excel, Visio (TM), etc. To access these nested objects, often the servers provide methods or properties whose type is a kind of object pointer. In some cases, the server documentation describes the type returned by such methods/properties as "LPDISPATCH", or "PDISP", or a similar type. With the TOleAutoClient component, the type name you use for this purpose is 'pInterface'; it means essentially the same thing as LPDISPATCH, which is one of the type names often used in C/C++ language documentation of OLE automation. ****************************************** EXAMPLE 2 - (NUMSVR.ZIP) ****************************************** As another example, let's look at a small OLE Automation server called NUMSVR1.EXE, distributed along with this component. This server simulates an integer property by exposing read and write methods GetX and SetX, and one method, named Incr, that accepts a single integer parameter PASSED BY REFERENCE. The Incr() method's only effect is to increment its argument. In the NumSvr example program, you can watch how your Delphi program passes an integer variable to the server which ends up being incremented as a result of the call to Incr(). This is a useful feature not (yet) supported by Visual Basic. The "mirror" class TNumSvrOb is contained in the NUMSVCLS.PAS unit; its interface section exactly reflects the OLE server's exposed methods, and the implementation is straightforward. Notice how the argument to the Incr method is passed via SetOleMethodArg with a type name of '&integer' to indicate that the argument is being passed by reference rather than by value. Also notice how the function GetX is implemented. Here the call to CallOleFunction specifies the name of the OLE method ('GetX'), the return type ('integer') and the name of the variable into which the return value is to be put. unit Numsvcls; interface uses SysUtils, OleAuto; type TNumSvrOb = class(TOleObject) private procedure SetX(newx : Integer); function GetX : Integer; public property X : integer read GetX write SetX; procedure Incr(var x : Integer); end; implementation procedure TNumSvrOb.Incr(var x : Integer); begin { Specify argument type as '&Integer' to pass by reference. } SetOleMethodArg('&Integer', x); CallOleProc('Incr'); end; procedure TNumSvrOb.SetX(newx : Integer); begin SetOleMethodArg('integer', newx); CallOleProc('SetX'); end; function TNumSvrOb.GetX : Integer; var x : Integer; begin CallOleFunction('GetX', 'Integer', x); Result := x; end; end. ****************************************** ERROR CHECKING AND HANDLING ****************************************** ++++++++++++++++++++++++++++++ Inability to start up a server ++++++++++++++++++++++++++++++ The CreateObject and CreateInProcessObject constructors for your mirror class, inherited from TOleObject, will raise an EOleAutoNoCreate exception if an OLE server cannot be started up successfully. This can happen in the following situations: a) The ProgID string that identifies the server object can't be found in your system registry (REG.DAT under Windows 3.x). You can inspect the registry by running REGEDIT /V from Program Manager. This usually means that the server has never registered itself, but may mean that the registry has become corrupted. With many OLE Automation servers, you need to run the server application under Windows at least once as a stand-alone application so that it can register itself. Alternatively, some servers come with setup or installation programs that accomplish this task. b) Windows can't locate the executable file or DLL containing the server in the place it expects to find it. This may mean that the server application has been moved or re-installed into a different directory than the one from which it was originally installed or registered. Re-installing the server application will correct this problem. c) Either Windows itself or one of its OLE support DLLs encountered an error trying to start up the server application. This can occur as a result of a bug in the server itself, or may be due to a system failure, Windows GPF, etc. during the debugging process. It is also possible that the versions of the Windows OLE system DLLs (normally installed by Windows in \WINDOWS\SYSTEM) are obsolete; in particular, the versions of COMPOBJ.DLL, OLE2DISP.DLL and OLE2.DLL should be no earlier than version 2.02. d) Programmers sometimes forget to place a TOleAutoClient component on a form whose unit uses the component. The application will compile correctly, but since Delphi will never call the TOLEAutoClient constructor, the Windows OLE libraries will never be correctly initialised. Mysteriously, although this bug can happen under Windows 3.x, the application will run successfully under Windows 95! You can handle the EOleAutoNoCreate exception yourself with a try block or let Delphi handle it for you. Refer to the file WHATSNEW.TXT for additional information regarding exceptions raised by the GetObject mirror class constructor used to "hook into" and control a running instance of an OLE Automation server. If there is insufficient memory to complete a SetOleMethodArg call, typically with a string argument, the TOleAutomationClient component will raise an EOleAutoOutOfMemory exception. In the unlikely event this occurs, you can handle the exception yourself or let Delphi handle it. +++++++++++++++++++++++++++++++++++++++ OLE method calls don't execute properly +++++++++++++++++++++++++++++++++++++++ Attempts to retrieve or set a server's properties or to call its methods will not generate exceptions if they fail. The most common situation in which this will occur is when there is a type mismatch between the mirror class and the server, either for a property or a method argument or return type. Some servers will attempt to coerce an argument to the correct type, but not all. Also, some server methods may be incorrectly documented as functions returning a type of VT_EMPTY (corresponding to a Delphi type string of '', an empty string) when they really should be implemented as procedures with no return value. Finally, some methods may allow "optional" arguments to be omitted, while others may require all specified arguments to be present and will cause en error if the number of arguments is incorrect. Check your server documentation carefully (if there is any). The only argument/method/property types supported by TOLEAutoClient are those documented in the OLEAUTO.INT file. Note also that the PInterface type referred to in this documentation may be referred to as "pIDisp", "pDisp", "pIDispatch" or "LPDISPATCH" in some server documentation. Your mirror class should be written to pass 'pInterface' as a type name in such cases. You can check for success or failure of a call to any of CallOleProc, CallOleFunction, GetOleProperty or SetOleProperty by a call to the GetOleMethodFailFlag method, declared in the TOleObject class as function TOleObject.GetOleAutoFailFlag : Boolean; This returns an internal flag which will be True if the call failed (i.e., if the Windows OLE DLLs returned with an error indication) or False if the call was executed successfully. An alternative and equivalent way to check this status is to refer to the OleAutoFailFlag property of your mirror class object descended from TOleObject. For more detailed information regarding the nature of an error generated during one of the above calls, you can access the OleAutoErrCode property (of type HRESULT) of your mirror class object descended from TOleObject. If there has been no error, this property should have the value S_OK. Refer to the OLEAUTO.INT file for details of the error codes returned through this property. Alternatively, if you prefer to use Delphi's exception-handling mechanism to signal failure of OLE method calls or property accesses, the code given below makes this easy to accomplish. You simply derive an intermediate class, say TOleObjectEx, from TOleObject and use this intermediate class instead of TOleObject as the ancestor from which you derive your mirror class. Working code for this class appears below. You can place it in a new unit called OLEAUTX.PAS that you add to your project. Be sure to replace OLEAUTO with OLEAUTX in the Uses clause of your mirror class unit so your mirror class can "see" TOleObjectEx. Note that the code below displays exception messages that don't depend on the specific value of OleAutoErrCode. Feel free to enhance the logic shown below, if you like, by comparing the value of OleAutoErrCode to the various E_xxxxx error code values defined in OLEAUTO.INT and varying the exception messages accordingly. { OLEAUTX.PAS (C) 1995 W. Raike } unit OleAutX; {*******************************************************************} { Class TOLEAutoEx derived from TOleObject overrides virtual methods for OLE property/method access in order to raise an exception when such a method call is unsuccessful. } {*******************************************************************} interface uses OleAuto; type TOleObjectEx = class(TOleObject) procedure CallOleFunction(sMethodName : String; sReturnType : String; var Retval); override; procedure CallOleProc(sMethodName : String); override; procedure GetOleProperty(sPropertyName : String; sPropertyType : String; var RetVal); override; procedure SetOleProperty(sPropertyName : String; sPropertyType : String; var PropVal); override; end; implementation procedure TOleObjectEx.CallOleFunction(sMethodName : String; sReturnType : String; var Retval); begin inherited CallOleFunction(sMethodName, sReturnType, Retval); if OleAutoErrCode <> S_OK then raise EOleAutoInvokeFailure.Create('OLE function call failed'); end; procedure TOleObjectEx.CallOleProc(sMethodName : String); begin inherited CallOleProc(sMethodName); if OleAutoErrCode <> S_OK then raise EOleAutoInvokeFailure.Create('OLE procedure call failed'); end; procedure TOleObjectEx.GetOleProperty(sPropertyName : String; sPropertyType : String; var RetVal); begin inherited GetOleProperty(sPropertyName, sPropertyType, Retval); if OleAutoErrCode <> S_OK then raise EOleAutoInvokeFailure.Create('OLE property get failed'); end; procedure TOleObjectEx.SetOleProperty(sPropertyName : String; sPropertyType : String; var PropVal); begin inherited SetOleProperty(sPropertyName, sPropertyType, PropVal); if OleAutoErrCode <> S_OK then raise EOleAutoInvokeFailure.Create('OLE property set failed'); end; end.